package com.moneycollect.example.classloader.testapp.config;

import com.moneycollect.example.classloader.loader.MyClassLoader;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

import java.lang.reflect.Constructor;
import java.util.ArrayList;

/**
 * @author xuhuiqian
 * @since 2022/5/27 15:09
 */
@Configuration
public class DefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {
    // use static field for forbidding GC
    private static volatile MyClassLoader myClassLoader;
    private static final String CONFIG_PREFIX = "encrypt-class";

    private Environment environment;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        // EncryptClassConfig can not use @ConfigurationProperties and Autowired here，because config bean have not
        // initial finish, so use Binder to bind the config to EncryptClassConfig
        BindResult<EncryptClassConfig> bindResult = Binder.get(environment).bind(CONFIG_PREFIX, EncryptClassConfig.class);
        var encryptClassConfig = bindResult.get();

        if (encryptClassConfig == null || encryptClassConfig.getClassList() == null ||
                encryptClassConfig.getClassList().size() < 1 || encryptClassConfig.getFiles() == null ||
                encryptClassConfig.getFiles().size() < 1) {
            return;
        }

        for (var clzName : encryptClassConfig.getClassList()) {
            var bean = getEncryptClassBean(clzName, encryptClassConfig);
            registerBean(beanDefinitionRegistry, bean.getClass().getName(), bean.getClass());
        }
    }

    private Object getEncryptClassBean(String clzName, EncryptClassConfig encryptClassConfig) {
        try {
            if (myClassLoader == null) {
                synchronized (MyClassLoader.class) {
                    if (myClassLoader == null) {
                        myClassLoader = new MyClassLoader("MyClassLoader", this.getClass().getClassLoader());
                        myClassLoader.setEncryptedFileList(new ArrayList<>());
                        myClassLoader.getEncryptedFileList().addAll(encryptClassConfig.getFiles());
                    }
                }
            }

            Class<?> clz = myClassLoader.loadClass(clzName);
            Constructor<?> constructor = clz.getConstructor();
            constructor.setAccessible(true);
            return constructor.newInstance();
        } catch (Exception e) {
            throw new RuntimeException("handle encrypt class bean error:" + e.getMessage(), e);
        }
    }

    private void registerBean(BeanDefinitionRegistry registry, String name, Class<?> beanClass) {
        RootBeanDefinition bean = new RootBeanDefinition(beanClass);
        registry.registerBeanDefinition(name, bean);
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }
}
